查看原文
其他

聊一聊Spring在单例模式下的线程安全

Java知音 2022-09-05

点击关注公众号,实用技术文章及时了解

来源:blog.csdn.net/fuzhongmin05/

article/details/100849867

1、有状态的bean与无状态的bean

有状态bean: 每个用户有自己特有的一个实例,在用户的生存期内,bean保存了用户的信息,即有状态;一旦用户灭亡(调用结束或实例结束),bean的生命期也告结束。即每个用户最初都会得到一个初始的bean。

无状态bean: bean一旦实例化就被加进会话池中,各个用户都可以共用。即使用户已经消亡,bean的生命期也不一定结束,它可能依然存在于会话池中,供其他用户调用。由于没有特定的用户,那么也就不能保持某一用户的状态,所以叫无状态bean。但无状态会话bean 并非没有状态,如果它有自己的属性(变量)。

有状态就是有数据存储功能。 有状态对象(Stateful Bean),就是有实例变量的对象 ,可以保存数据,是非线程安全的。在不同方法调用间不保留任何状态。

无状态就是一次操作不能保存数据。 无状态对象(Stateless Bean),就是没有实例变量的对象 ,不能保存数据是不变类,是线程安全的。

在Spring的Bean配置中,存在这样两种情况:

<bean id="testManager" class="com.sw.TestManagerImpl" scope="singleton" />  
<bean id="testManager" class="com.sw.TestManagerImpl" scope="prototype" /> 

当然,scope的值不止这两种,还包括了request、session 等。但用的最多的还是singleton单态与prototype多态。

  • singleton表示该bean全局只有一个实例,Spring中bean的scope默认也是singleton。
  • prototype表示该bean在每次被注入的时候,都要重新创建一个实例,这种情况适用于有状态的Bean。

下面是一个有状态的Bean示例

public class TestManagerImpl implements TestManager {  
 private User user;
 public void deleteUser(User e) throws Exception {  
        user = e; //1
        prepareData(e);
    }

    public void prepareData(User e) throws Exception {  
  user = getUserByID(e.getId());  //2
  //使用user.getId();    //3
    }

}

如果该Bean配置为singleton,会出现什么样的状况呢?

如果有2个用户访问,都调用到了该Bean,假定为user1、user2。

当user1调用到程序中的步骤1的时候,该Bean的私有变量user被赋值为user1,当user1的程序走到步骤2的时候,该Bean的私有变量user被重新赋值为user1_create,理想的状况,当user1走到3步骤的时候,私有变量user应该为user1_create;但如果在user1调用到3步骤之前,user2开始运行到了步骤1了,由于单态的资源共享,则私有变量user被修改为user2;这种情况下,user1的步骤3用到的user.getId()实际用到是user2的对象。

即将有状态的bean配置成singleton会造成资源混乱问题(线程安全问题),而如果是prototype的话,就不会出现资源共享的问题,即不会出现线程安全的问题。

注:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,那么代码就是线程安全的。

通过上面分析,大家已经对有状态和无状态有了一定的理解。无状态的Bean适合用不变模式,技术就是单例模式,这样可以共享实例,提高性能。有状态的Bean,多线程环境下不安全,那么适合用Prototype原型模式(解决多线程问题),每次对bean的请求都会创建一个新的bean实例。

2、Spring中的单例

Spring中的单例与设计模式里面的单例略有不同,设计模式的单例是在整个应用中只有一个实例,而Spring中的单例是在一个IOC容器中就只有一个实例。

大多数时候客户端都在访问我们应用中的业务对象,为了减少并发控制,在这个时候我们不应该在业务对象中设置那些容易造成出错的成员变量。在并发访问时候,这些成员变量将会是并发线程中的共享对象,那么这个时候就会出现意外情况。

成员变量的解决方式:

  • 方法的参数局部变量(在方法中new)
  • 使用Threadlocal
  • 设置bean的scope=prototype

3、Spring使用ThreadLocal解决线程安全问题案例

Spring作为一个IOC容器,帮助我们管理了许许多多的bean。但其实,Spring并没有保证这些对象的线程安全,需要由开发者自己编写解决线程安全问题的代码。

在使用Spring时,很多人可能对Spring中为什么DAO和Service对象采用单实例方式很迷惑,这些读者是这么认为的。

DAO对象必须包含一个数据库的连接Connection,而这个Connection不是线程安全的,所以每个DAO都要包含一个不同的Connection对象实例,这样一来DAO对象就不能是单实例的了。

上述观点对了一半。对的是“每个DAO都要包含一个不同的Connection对象实例”这句话,错的是“DAO对象就不能是单实例”。其实Spring在实现Service和DAO对象时,使用了ThreadLocal这个类,这个是一切的核心!

ThreadLocal

  • 每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。
  • 将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。

一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程。这样你就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有关联的对象引用到的都是同一个变量。

下面的实例能够体现Spring对有状态Bean的改造思路:

public class TopicDao {
 //①一个非线程安全的变量
  private Connection conn;
 //②引用非线程安全变量
  public void addTopic() {
  Statement stat = conn.createStatement();
  }
}

由于①处的conn是成员变量,因为addTopic()方法是非线程安全的,必须在使用时创建一个新TopicDao实例(非singleton)。下面使用ThreadLocal对conn这个非线程安全的状态进行改造:

public class TopicDao {  
    //①使用ThreadLocal保存Connection变量  
    private static ThreadLocal <Connection>connThreadLocal = newThreadLocal<Connection>();  
    public static Connection getConnection() {  
       // ②如果connThreadLocal没有本线程对应的Connection创建一个新的Connection,  
       // 并将其保存到线程本地变量中。  
       if (connThreadLocal.get() == null) {  
           Connection conn = getConnection();  
           connThreadLocal.set(conn);  
           return conn;  
       }
       // ③直接返回线程本地变量
       return connThreadLocal.get();  
    }

    public void addTopic() {  
       // ④从ThreadLocal中获取线程对应的Connection  
       try {
           Statement stat = getConnection().createStatement();  
       } catch (SQLException e) {  
           e.printStackTrace();  
       }
    }
}

不同的线程在使用TopicDao时,先判断connThreadLocal是否是null,如果是null,则说明当前线程还没有对应的Connection对象,这时创建一个Connection对象并添加到本地线程变量中;如果不为null,则说明当前的线程已经拥有了Connection对象,直接使用就可以了。这样,就保证了不同的线程使用线程相关的Connection,而不会使用其它线程的Connection。因此,这个TopicDao就可以做到singleton共享了。

Spring中DAO和Service都是以单实例的bean形式存在,Spring通过ThreadLocal类将有状态的变量(例如数据库连接Connection)本地线程化,从而做到多线程状况下的安全。在一次请求响应的处理线程中, 该线程贯通展示、服务、数据持久化三层,通过ThreadLocal使得所有关联的对象引用到的都是同一个变量。

当然,这个例子本身很粗糙,将Connection的ThreadLocal直接放在DAO只能做到本DAO的多个方法共享Connection时不发生线程安全问题,但无法和其它DAO共用同一个Connection,要做到同一事务多DAO共享同一Connection,必须在一个共同的外部类使用ThreadLocal保存Connection。

Web应用划分为展现层、服务层和持久层,controller中引入xxxService作为成员变量,xxxService中又引入xxxDao作为成员变量,这些对象都是单例而且会被多个线程并发访问,可我们访问的是它们里面的方法,这些类里面通常不会含有成员变量,dao实例是在MyBatis等ORM框架里面封装好的,已经被测试,不会出现线程同步问题了。所以出问题的地方就是我们自己系统里面的业务对象,所以我们一定要注意自定义的业务对象里面千万不能出现独立成员变量,否则会有线程安全问题。

通常我们在应用中的业务对象如下例子,controller中拥有成员变量list和paperService。

public class TestPaperController extends BaseController {

 private static final int list = 0;

 @Autowired
 @Qualifier("papersService")
 private TestPaperService papersService ;

 public Page queryPaper(int pageSize, int page,TestPaper paper) throws EicException {
   RowSelection localRowSelection = getRowSelection(pageSize, page);
   List<TestPaper> paperList = papersService.queryPaper(paper,localRowSelection);
   Page localPage = new Page(page, localRowSelection.getTotalRows(), paperList);
   return localPage;
 }
}

service里面又引入了成员变量ibatisEntityDao

@SuppressWarnings("unchecked")
@Service("papersService")
@Transactional(rollbackFor = {Exception.class})
public class TestPaperServiceImpl implements TestPaperService 
{

 @Autowired
 @Qualifier("ibatisEntityDao")
 private IbatisEntityDao ibatisEntityDao;

 private static final String NAMESPACE_TESTPAPER = "com.its.exam.testpaper.model.TestPaper";

 private static final String BO_NAME[] = { "试卷仓库" };

 private static final String BO_NAME2[] = { "试卷配置试题" };

 private static final String BO_NAME1[] = { "试卷试题类型" };

 private static final String NAMESPACE_TESTQUESTION="com.its.exam.testpaper.model.TestQuestion";

 public List<TestPaper> queryPaper(TestPaper paper,RowSelection paramRowSelection) throws EicException {
   try {
    return (List<TestPaper>) ibatisEntityDao.queryForListWithPage(NAMESPACE_TESTPAPER, "queryPaper", paper,paramRowSelection);
   } catch (Exception e) {
    e.printStackTrace();
    throw new EicException(e, "eic""0001", BO_NAME);
   }
 }
}

由上面例子可以看出,虽然我们这个应用里面含有成员变量,但是并不会出现线程同步方面的问题,controller里面的成员变量papersService被注入后,是为了访问该service类的方法,papersService里面注入的成员变量ibatisEntityDao是ORM框架封装好的,其线程同步问题已解决。

4、ThreadLocal与线程同步机制的比较

ThreadLocal和线程同步机制相比有什么优势呢?ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。

在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。

而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。

由于ThreadLocal中可以持有任何类型的对象,低版本JDK所提供的get()返回的是Object对象,需要强制类型转换。但JDK 5.0通过泛型很好的解决了这个问题,在一定程度地简化ThreadLocal的使用。

概括起来说,对于多线程资源共享的问题,同步机制采用了以时间换空间”的方式,而ThreadLocal采用了以空间换时间的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。另外,关注Java知音公众号,回复“后端面试”,送你一份面试题宝典!

ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。

参考资料

  • 聊一聊 Spring 中的线程安全性
  • spring 是如何解决并发访问的线程安全性问题的
  • Spring 如何保证线程安全
  • spring单例,为什么service和dao确能保证线程安全
【练手项目】基于SpringBoot的ERP系统,自带进销存+财务+生产功能分享一套基于SpringBoot和Vue的企业级中后台开源项目,代码很规范!能挣钱的,开源 SpringBoot 商城系统,功能超全,超漂亮!

PS:因为公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。点“在看”支持我们吧!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存